/*M///////////////////////////////////////////////////////////////////////////////////////
//
//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
//  By downloading, copying, installing or using the software you agree to this license.
//  If you do not agree to this license, do not download, install,
//  copy or use the software.
//
//
//                           License Agreement
//                For Open Source Computer Vision Library
//
// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
// Copyright (C) 2009, Willow Garage Inc., all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//   * Redistribution's of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//
//   * Redistribution's in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//   * The name of the copyright holders may not be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
//M*/

#include "test_precomp.hpp"

namespace opencv_test {
namespace {

class CV_ECC_BaseTest : public cvtest::BaseTest {
   public:
    CV_ECC_BaseTest();
    virtual ~CV_ECC_BaseTest();

   protected:
    double computeRMS(const Mat& mat1, const Mat& mat2);
    bool isMapCorrect(const Mat& mat);

    virtual bool test(const Mat) { return true; };  // single test
    bool testAllTypes(const Mat img);               // run test for all supported data types (U8, U16, F32, F64)
    bool testAllChNum(const Mat img);               // run test for all supported channels count (gray, RGB)

    void run(int);

    bool checkMap(const Mat& map, const Mat& ground);

    double MAX_RMS_ECC;  // upper bound for RMS error
    int ntests;          // number of tests per motion type
    int ECC_iterations;  // number of iterations for ECC
    double ECC_epsilon;  // we choose a negative value, so that
    // ECC_iterations are always executed
    TermCriteria criteria;
};

CV_ECC_BaseTest::CV_ECC_BaseTest() {
    MAX_RMS_ECC = 0.1;
    ntests = 3;
    ECC_iterations = 50;
    ECC_epsilon = -1;  //-> negative value means that ECC_Iterations will be executed
    criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, ECC_iterations, ECC_epsilon);
}

CV_ECC_BaseTest::~CV_ECC_BaseTest() {}

bool CV_ECC_BaseTest::isMapCorrect(const Mat& map) {
    bool tr = true;
    float mapVal;
    for (int i = 0; i < map.rows; i++)
        for (int j = 0; j < map.cols; j++) {
            mapVal = map.at<float>(i, j);
            tr = tr & (!cvIsNaN(mapVal) && (fabs(mapVal) < 1e9));
        }

    return tr;
}

double CV_ECC_BaseTest::computeRMS(const Mat& mat1, const Mat& mat2) {
    CV_Assert(mat1.rows == mat2.rows);
    CV_Assert(mat1.cols == mat2.cols);

    Mat errorMat;
    subtract(mat1, mat2, errorMat);

    return sqrt(errorMat.dot(errorMat) / (mat1.rows * mat1.cols * mat1.channels()));
}

bool CV_ECC_BaseTest::checkMap(const Mat& map, const Mat& ground) {
    if (!isMapCorrect(map)) {
        ts->set_failed_test_info(cvtest::TS::FAIL_INVALID_OUTPUT);
        return false;
    }

    if (computeRMS(map, ground) > MAX_RMS_ECC) {
        ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
        ts->printf(ts->LOG, "RMS = %f", computeRMS(map, ground));
        return false;
    }
    return true;
}

bool CV_ECC_BaseTest::testAllTypes(const Mat img) {
    auto types = {CV_8U, CV_16U, CV_32F, CV_64F};
    for (auto type : types) {
        Mat timg;
        img.convertTo(timg, type);
        if (!test(timg))
            return false;
    }
    return true;
}

bool CV_ECC_BaseTest::testAllChNum(const Mat img) {
    if (!testAllTypes(img))
        return false;

    Mat gray;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    if (!testAllTypes(gray))
        return false;

    return true;
}

void CV_ECC_BaseTest::run(int) {
    Mat img = imread(string(ts->get_data_path()) + "shared/fruits.png");
    if (img.empty()) {
        ts->printf(ts->LOG, "test image can not be read");
        ts->set_failed_test_info(cvtest::TS::FAIL_INVALID_TEST_DATA);
        return;
    }

    Mat testImg;
    resize(img, testImg, Size(216, 216), 0, 0, INTER_LINEAR_EXACT);

    testAllChNum(testImg);

    ts->set_failed_test_info(cvtest::TS::OK);
}

class CV_ECC_Test_Translation : public CV_ECC_BaseTest {
   public:
    CV_ECC_Test_Translation();

   protected:
    bool test(const Mat);
};

CV_ECC_Test_Translation::CV_ECC_Test_Translation() {}

bool CV_ECC_Test_Translation::test(const Mat testImg) {
    cv::RNG rng = ts->get_rng();

    int progress = 0;

    for (int k = 0; k < ntests; k++) {
        ts->update_context(this, k, true);
        progress = update_progress(progress, k, ntests, 0);

        Mat translationGround = (Mat_<float>(2, 3) << 1, 0, (rng.uniform(10.f, 20.f)), 0, 1, (rng.uniform(10.f, 20.f)));

        Mat warpedImage;

        warpAffine(testImg, warpedImage, translationGround, Size(200, 200), INTER_LINEAR + WARP_INVERSE_MAP);

        Mat mapTranslation = (Mat_<float>(2, 3) << 1, 0, 0, 0, 1, 0);

        findTransformECC(warpedImage, testImg, mapTranslation, 0, criteria);

        if (!checkMap(mapTranslation, translationGround))
            return false;
    }
    return true;
}

class CV_ECC_Test_Euclidean : public CV_ECC_BaseTest {
   public:
    CV_ECC_Test_Euclidean();

   protected:
    bool test(const Mat);
};

CV_ECC_Test_Euclidean::CV_ECC_Test_Euclidean() {}

bool CV_ECC_Test_Euclidean::test(const Mat testImg) {
    cv::RNG rng = ts->get_rng();

    int progress = 0;
    for (int k = 0; k < ntests; k++) {
        ts->update_context(this, k, true);
        progress = update_progress(progress, k, ntests, 0);

        double angle = CV_PI / 30 + CV_PI * rng.uniform((double)-2.f, (double)2.f) / 180;

        Mat euclideanGround = (Mat_<float>(2, 3) << cos(angle), -sin(angle), (rng.uniform(10.f, 20.f)), sin(angle),
                               cos(angle), (rng.uniform(10.f, 20.f)));

        Mat warpedImage;

        warpAffine(testImg, warpedImage, euclideanGround, Size(200, 200), INTER_LINEAR + WARP_INVERSE_MAP);

        Mat mapEuclidean = (Mat_<float>(2, 3) << 1, 0, 0, 0, 1, 0);

        findTransformECC(warpedImage, testImg, mapEuclidean, 1, criteria);

        if (!checkMap(mapEuclidean, euclideanGround))
            return false;
    }
    return true;
}

class CV_ECC_Test_Affine : public CV_ECC_BaseTest {
   public:
    CV_ECC_Test_Affine();

   protected:
    bool test(const Mat img);
};

CV_ECC_Test_Affine::CV_ECC_Test_Affine() {}

bool CV_ECC_Test_Affine::test(const Mat testImg) {
    cv::RNG rng = ts->get_rng();

    int progress = 0;
    for (int k = 0; k < ntests; k++) {
        ts->update_context(this, k, true);
        progress = update_progress(progress, k, ntests, 0);

        Mat affineGround = (Mat_<float>(2, 3) << (1 - rng.uniform(-0.05f, 0.05f)), (rng.uniform(-0.03f, 0.03f)),
                            (rng.uniform(10.f, 20.f)), (rng.uniform(-0.03f, 0.03f)), (1 - rng.uniform(-0.05f, 0.05f)),
                            (rng.uniform(10.f, 20.f)));

        Mat warpedImage;

        warpAffine(testImg, warpedImage, affineGround, Size(200, 200), INTER_LINEAR + WARP_INVERSE_MAP);

        Mat mapAffine = (Mat_<float>(2, 3) << 1, 0, 0, 0, 1, 0);

        findTransformECC(warpedImage, testImg, mapAffine, 2, criteria);

        if (!checkMap(mapAffine, affineGround))
            return false;
    }

    return true;
}

class CV_ECC_Test_Homography : public CV_ECC_BaseTest {
   public:
    CV_ECC_Test_Homography();

   protected:
    bool test(const Mat testImg);
};

CV_ECC_Test_Homography::CV_ECC_Test_Homography() {}

bool CV_ECC_Test_Homography::test(const Mat testImg) {
    cv::RNG rng = ts->get_rng();

    int progress = 0;
    for (int k = 0; k < ntests; k++) {
        ts->update_context(this, k, true);
        progress = update_progress(progress, k, ntests, 0);

        Mat homoGround =
            (Mat_<float>(3, 3) << (1 - rng.uniform(-0.05f, 0.05f)), (rng.uniform(-0.03f, 0.03f)),
             (rng.uniform(10.f, 20.f)), (rng.uniform(-0.03f, 0.03f)), (1 - rng.uniform(-0.05f, 0.05f)),
             (rng.uniform(10.f, 20.f)), (rng.uniform(0.0001f, 0.0003f)), (rng.uniform(0.0001f, 0.0003f)), 1.f);

        Mat warpedImage;

        warpPerspective(testImg, warpedImage, homoGround, Size(200, 200), INTER_LINEAR + WARP_INVERSE_MAP);

        Mat mapHomography = Mat::eye(3, 3, CV_32F);

        findTransformECC(warpedImage, testImg, mapHomography, 3, criteria);

        if (!checkMap(mapHomography, homoGround))
            return false;
    }
    return true;
}

class CV_ECC_Test_Mask : public CV_ECC_BaseTest {
   public:
    CV_ECC_Test_Mask();

   protected:
    bool test(const Mat);
};

CV_ECC_Test_Mask::CV_ECC_Test_Mask() {}

bool CV_ECC_Test_Mask::test(const Mat testImg) {
    cv::RNG rng = ts->get_rng();

    int progress = 0;

    for (int k = 0; k < ntests; k++) {
        ts->update_context(this, k, true);
        progress = update_progress(progress, k, ntests, 0);

        Mat translationGround = (Mat_<float>(2, 3) << 1, 0, (rng.uniform(10.f, 20.f)), 0, 1, (rng.uniform(10.f, 20.f)));

        Mat warpedImage;

        warpAffine(testImg, warpedImage, translationGround, Size(200, 200), INTER_LINEAR + WARP_INVERSE_MAP);

        Mat mapTranslation = (Mat_<float>(2, 3) << 1, 0, 0, 0, 1, 0);

        Mat_<unsigned char> mask = Mat_<unsigned char>::ones(testImg.rows, testImg.cols);
        Rect region(testImg.cols * 2 / 3, testImg.rows * 2 / 3, testImg.cols / 3, testImg.rows / 3);

        rectangle(testImg, region, Scalar::all(0), FILLED);
        rectangle(mask, region, Scalar(0), FILLED);

        findTransformECC(warpedImage, testImg, mapTranslation, 0, criteria, mask);

        if (!checkMap(mapTranslation, translationGround))
            return false;

        // Test with non-default gaussian blur.
        findTransformECC(warpedImage, testImg, mapTranslation, 0, criteria, mask, 1);

        if (!checkMap(mapTranslation, translationGround))
            return false;

        // Test with template mask.
        Mat_<unsigned char> warpedMask = Mat_<unsigned char>::ones(warpedImage.rows, warpedImage.cols);
        for (int i=warpedImage.rows*1/3; i<warpedImage.rows*2/3; i++) {
            for (int j=warpedImage.cols*1/3; j<warpedImage.cols*2/3; j++) {
                warpedMask(i, j) = 0;
            }
        }

        findTransformECCWithMask(warpedImage, testImg, warpedMask, mask, mapTranslation, 0,
                    TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, ECC_iterations, ECC_epsilon));

        if (!checkMap(mapTranslation, translationGround))
            return false;

        // Test with non-default gaussian blur.
        findTransformECCWithMask(warpedImage, testImg, warpedMask,  mask, mapTranslation, 0, criteria, 1);

        if (!checkMap(mapTranslation, translationGround))
            return false;
    }
    return true;
}

void testECCProperties(Mat x, float eps) {
    // The channels are independent
    Mat y = x.t();
    Mat Z = Mat::zeros(x.size(), y.type());
    Mat O = Mat::ones(x.size(), y.type());

    EXPECT_NEAR(computeECC(x, y), 0.0, eps);
    if (x.type() != CV_8U && x.type() != CV_8U) {
        EXPECT_NEAR(computeECC(x + y, x - y), 0.0, eps);
    }

    EXPECT_NEAR(computeECC(x, x), 1.0, eps);

    Mat R, G, B, X, Y;
    cv::merge(std::vector<cv::Mat>({O, Z, Z}), R);
    cv::merge(std::vector<cv::Mat>({Z, O, Z}), G);
    cv::merge(std::vector<cv::Mat>({Z, Z, O}), B);
    cv::merge(std::vector<cv::Mat>({x, x, x}), X);
    cv::merge(std::vector<cv::Mat>({y, y, y}), Y);

    // 1. The channels are orthogonal and independent
    EXPECT_NEAR(computeECC(X.mul(R), X.mul(G)), 0, eps);
    EXPECT_NEAR(computeECC(X.mul(R), X.mul(B)), 0, eps);
    EXPECT_NEAR(computeECC(X.mul(B), X.mul(G)), 0, eps);

    EXPECT_NEAR(computeECC(X.mul(R) + Y.mul(B), X.mul(B) + Y.mul(R)), 0, eps);

    EXPECT_NEAR(computeECC(X.mul(R) + Y.mul(G) + (X + Y).mul(B), Y.mul(R) + X.mul(G) + (X - Y).mul(B)), 0, eps);

    // 2. Each channel contribute equally
    EXPECT_NEAR(computeECC(X.mul(R) + Y.mul(G + B), X), 1.0 / 3, eps);
    EXPECT_NEAR(computeECC(X.mul(G) + Y.mul(R + B), X), 1.0 / 3, eps);
    EXPECT_NEAR(computeECC(X.mul(B) + Y.mul(G + R), X), 1.0 / 3, eps);

    // 3. The coefficient is invariant with respect to the offset of channels
    EXPECT_NEAR(computeECC(X - R + 2 * G + B, X), 1.0, eps);
    if (x.type() != CV_8U && x.type() != CV_8U) {
        EXPECT_NEAR(computeECC(X + R - 2 * G + B, Y), 0.0, eps);
    }

    // The channels are independent. Check orthogonal combinations
    // full squares norm = sum of squared norms
    EXPECT_NEAR(computeECC(X, Y + X), 1.0 / sqrt(2.0), eps);
    EXPECT_NEAR(computeECC(X, 2 * Y + X), 1.0 / sqrt(5.0), eps);
}

TEST(Video_ECC_Test_Compute, properies) {
    Mat xline(1, 100, CV_32F), x;
    for (int i = 0; i < xline.cols; ++i) xline.at<float>(0, i) =  (float)i;

    repeat(xline, xline.cols, 1, x);

    Mat x_f64, x_u8, x_u16;
    x.convertTo(x_f64, CV_64F);
    x.convertTo(x_u8, CV_8U);
    x.convertTo(x_u16, CV_16U);

    testECCProperties(x, 1e-5f);
    testECCProperties(x_f64, 1e-5f);
    testECCProperties(x_u8, 1);
    testECCProperties(x_u16, 1);
}

TEST(Video_ECC_Test_Compute, accuracy) {
    Mat testImg = (Mat_<float>(3, 3) << 1, 0, 0, 1, 0, 0, 1, 0, 0);
    Mat warpedImage = (Mat_<float>(3, 3) << 0, 1, 0, 0, 1, 0, 0, 1, 0);
    Mat_<unsigned char> mask = Mat_<unsigned char>::ones(testImg.rows, testImg.cols);
    double ecc = computeECC(warpedImage, testImg, mask);

    EXPECT_NEAR(ecc, -0.5f, 1e-5f);
}

TEST(Video_ECC_Test_Compute, bug_14657) {
    /*
     * Simple test case - a 2 x 2 matrix with 10, 10, 10, 6. When the mean (36 / 4 = 9) is subtracted,
     * it results in 1, 1, 1, 0 for the unsigned int case - compare to  1, 1, 1, -3 in the signed case.
     * For this reason, when the same matrix was provided as the input and the template, we didn't get 1 as expected.
     */
    Mat img = (Mat_<uint8_t>(2, 2) << 10, 10, 10, 6);
    EXPECT_NEAR(computeECC(img, img), 1.0f, 1e-5f);
}

TEST(Video_ECC_Translation, accuracy) {
    CV_ECC_Test_Translation test;
    test.safe_run();
}
TEST(Video_ECC_Euclidean, accuracy) {
    CV_ECC_Test_Euclidean test;
    test.safe_run();
}
TEST(Video_ECC_Affine, accuracy) {
    CV_ECC_Test_Affine test;
    test.safe_run();
}
TEST(Video_ECC_Homography, accuracy) {
    CV_ECC_Test_Homography test;
    test.safe_run();
}
TEST(Video_ECC_Mask, accuracy) {
    CV_ECC_Test_Mask test;
    test.safe_run();
}

}  // namespace
}  // namespace opencv_test
